// Copyright (C) 2023 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
// 
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<北京小羊驼科技有限公司>，授权编号：<NT231212144350>，代码指纹：<A.702363430.550>，技术对接人微信：<ID: wxid_wbyootvkdcgj22>】
// 【授权寄送：<收件：苏先生、地址：北京市大兴区北京密码西区6号楼1单元301、电话：18613885610、邮箱：bd@huiyuanxiang-inc.com>】
// 
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
// 
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rbchat.logic.main.loginimpl;

import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.eva.android.AutoUpdateDaemon;
import com.eva.android.widget.DataLoadingAsyncTask;
import com.eva.android.widget.WidgetUtils;
import com.eva.android.widget.alert.AlertDialog;
import com.eva.epc.common.util.CommonUtils;
import com.eva.framework.dto.DataFromServer;
import com.eva.framework.dto.LoginInfo2;
import com.google.gson.Gson;
import com.x52im.rbchat.Const;
import com.x52im.rbchat.MyApplication;
import com.x52im.rbchat.R;
import com.x52im.rainbowchat.common.dto.AutoUpdateInfoFromServer;
import com.x52im.rainbowchat.http.logic.dto.RosterElementEntity;
import com.x52im.rbchat.logic.chat_friend.vv.VVP2PProvider;
import com.x52im.rbchat.logic.chat_root.model.BusinessIntelligence;
import com.x52im.rbchat.network.http.HttpRestHelper;
import com.x52im.rbchat.utils.IntentFactory;
import com.x52im.rbchat.utils.PreferencesToolkits;

import net.x52im.mobileimsdk.android.core.LocalDataSender;
import net.x52im.mobileimsdk.server.protocal.c.PLoginInfo;

import java.util.Observable;
import java.util.Observer;
/**
 * 用于http登陆认证、im socket长连接建立的实用辅助类。
 *
 * -----------------------------------------------------------------------------------
 *【关于RainbowChat登陆/连接的说明】：
 *  1）目前RainbowChat的客户端（包括ios和android版）登陆/连接过程是分为2步的：
 *     1.1) 第一步是先提交用户的登陆名和密码到通用的http rest登陆认证接口，服务端
 *          返回用户id和token（还包括其它个人信息）；
 *     1.2) 第二步是在第一步http认证成功后，才将第一步反回的用户id和token用于连接
 *          IM服务器，IM服务器合法连接成功才算完成整个登陆/连接过程。
 *  2）这样分2步的目的是为了提升整个服务端的通用性，这样http服务端和im服务端很容
 *      易独立拆分和部署（实际上最佳实践就应该拆分），而且2者本身就有不同的用途（分
 *      别对应的是短连接和长连接服务），而且在大型应用中这个http的认证接口完全可以独
 *      立为更加通用的SSO单点登陆子系统，所以请正确地理解这个共二步的登陆和连接过程，
 *      从而灵活地部署您的服务端。
 *  3）关于连接IM服务器时loginUserId和loginToken在MobileIMSDK中的定义
 *     loginUserId - 提交到服务端的准一id，保证唯一就可以通信，可能是登陆用户名、 也
 *                   可能是任意不重复的id等，具体意义由业务层决定；
 *     loginToken - 提交到服务端用于身份鉴别和合法性检查的token，它可能是登陆密码 ，也
 *                  可能是通过前置单点登陆接口拿到的token等，具体意义由业务层决定；
 *     这实际上意味着连接IM服务端时的loginUserId和loginToken有很大的灵活性，对于IM对
 *     接已有系统、对接通用OAuth2.0系统等SSO单点登陆系统都会非常容易。
 * ----------------------------------------------------------------------------------
 *
 * @author Jack Jiang
 * @since 6.0
 */
public class LoginHelper
{
    public final static String TAG = LoginHelper.class.getSimpleName();

    /** 客户端版本码：读取自程序apk文件的AndroidManifest.xml中的同名属性.  */
    private static int versionCode = 0;
    /** 客户端版本名：读取自程序apk文件的AndroidManifest.xml中的同名属性.  */
    private static String versionName = "";

    static{
        initProgrammVersion();
    }

    /**
     * <p>
     * 初始化版本信息.<br>
     *
     * 读取程序apk文件的AndroidManifest.xml中的versionCode和versionName属性
     * 并存储在本类同名字段中以备后用.
     * </p>
     */
    private static void initProgrammVersion()
    {
        PackageInfo info;
        try
        {
            Context context = MyApplication.getInstance2();
            if(context != null) {
                info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
                versionCode = info.versionCode;
                versionName = info.versionName;
            }
        }
        catch (PackageManager.NameNotFoundException e)
        {
//            WidgetUtils.showToast(this, $$(R.string.login_form_version_error), WidgetUtils.ToastType.WARN);
            Log.d(TAG, "读程序版本信息时出错,"+e.getMessage(),e);
        }
    }

    /**
     * 返回本程序对应APK文件的versionCode.
     *
     * @return
     */
    public static int getAPKVersionCode()
    {
        return versionCode;
    }
    /**
     * 返回本程序对应APK文件的versionName.
     *
     * @return
     */
    public static String getAPKVersionName()
    {
        return versionName;
    }

    /**
     * http登陆认证异步实现类。
     *
     * <pre>
     * 【注意】：IM完整登陆过程，包括两步：
     *  1）第一步：http登陆认证；
     *  2）第二步：im socket长连接的建立。
     *  以上有关登陆过程的详细说明，请见 {@link LoginHelper} 的类说明。
     * </pre>
     *
     * <p>
     * 说明：为了能减少一次独立的版本更新检查网络请求，本内部类已版本更新检查合并到了本http登陆认证代码中，仅此而已。
     */
    public static class Login$CheckUpdateAsyc extends DataLoadingAsyncTask<Object, Integer, DataFromServer>
    {
        private LoginInfo2 loginInfo = null;

        // http登陆认证失败后，要调用的观察者
        private Observer afterLoginHttpServerFailObs = null;
        // im长连接登陆路认证成功后，要调用的观察者
        private Observer afterLoginIMServerSucessObs = null;

        public Login$CheckUpdateAsyc(Activity parentActicity, LoginInfo2 loginInfo
                , Observer afterLoginHttpServerFailObs, Observer afterLoginIMServerSucessObs)
        {
            super(parentActicity, parentActicity.getResources().getString(R.string.login_form_loading_login));
            this.loginInfo = loginInfo;
            this.afterLoginHttpServerFailObs = afterLoginHttpServerFailObs;
            this.afterLoginIMServerSucessObs = afterLoginIMServerSucessObs;
        }

        private String $$(int stringResId)
        {
            return this.context.getResources().getString(stringResId);
        }

        /**
         * 在后台执行 {@link HttpRestHelper#submitLoginToServer(LoginInfo2)}
         * 实现登陆信息的提交和处于结果的读取 .
         *
         * @param parems 外界传进来的参数
         * @return 查询结果，将传递给onPostExecute(..)方法
         */
        @Override
        protected DataFromServer doInBackground(Object... parems)
        {
            return HttpRestHelper.submitLoginToServer(loginInfo);
        }

        /**
         * 本方法将在异步任务执行成后边后被调，用于处理服务端返回的登陆结果信息.
         *
         * @see AutoUpdateDaemon
         */
        @Override
        protected void onPostExecuteImpl(Object result)
        {
            if(result instanceof String)
            {
                JSONObject nwObj = JSONObject.parseObject((String)result);
                String updateInfoJson = nwObj.getString("update_info");
                String userInfoJson = nwObj.getString("authed_info");

                System.out.println("服务端返回>>updateInfoJson="+updateInfoJson+", userInfoJson="+userInfoJson);

                // http登陆验证失败
                if(userInfoJson == null)
                {
                    // FIXBUG: google统计到较大量IllegalArgumentException (@LoginActivity$UpdateClientAsyncTask:onPostExecuteImpl:1075) {main}
                    //         错误，此if语句是为了保证延迟线程里不会因Activity已被关闭而此处却要非法地执行show的情况（此判断可趁为安全的show方法哦！）
                    Activity parentActivity = getParentActivity();
                    if (parentActivity != null && !parentActivity.isFinishing())
                    {
                        new AlertDialog.Builder(super.context)
                                .setTitle(R.string.login_form_error_psw_tip)
                                .setMessage(R.string.login_form_error_psw_message)
                                .setPositiveButton(R.string.general_ok,  null)
                                .show();

                        if(this.afterLoginHttpServerFailObs != null)
                            this.afterLoginHttpServerFailObs.update(null, null);
                    }
                }
                // http登陆验证成功
                else
                {
                    if(this.loginInfo != null) {
                        // http登陆验证成陆后，将本次的用户名和密码保存，以备下次打开程序时能自动登陆
                        PreferencesToolkits.saveDefaultLoginName(super.context
                                , new LoginInfoToSave(this.loginInfo.getLoginName(), this.loginInfo.getLoginPsw()));
                    }

                    // 解析服务端返回的版本更新信息
                    final AutoUpdateInfoFromServer aui = new Gson().fromJson(updateInfoJson, AutoUpdateInfoFromServer.class);
                    // 解析服务端返回的本地用户信息
                    final RosterElementEntity userAuthedInfo = HttpRestHelper.parseLoginFromServer(userInfoJson);

                    // 首先看看需不需要更新
                    if(aui.isNeedUpdate())
                    {
                        Log.d(TAG, "isNeedUpdate?"+aui.isNeedUpdate()
                                +",getLatestClientAPKVercionCode="+aui.getLatestClientAPKVercionCode()
                                +",getLatestClientAPKFileSize="+aui.getLatestClientAPKFileSize()
                                +",getLatestClientAPKURL"+aui.getLatestClientAPKURL());

                        new AlertDialog.Builder(super.context)
                                .setCancelable(false)
                                .setTitle($$(R.string.login_form_have_latest_version))
                                .setMessage($$(R.string.login_form_have_latest_version_descrption))
                                .setPositiveButton($$(R.string.login_form_have_latest_version_update_now),  new DialogInterface.OnClickListener(){
                                    @Override
                                    public void onClick(DialogInterface dialog, int which)
                                    {
                                        Activity parentActivity = getParentActivity();
                                        if(parentActivity != null) {
                                            //进入版本更新处理类进行更新处理
                                            AutoUpdateDaemon up = new AutoUpdateDaemon(parentActivity
                                                    , aui.getLatestClientAPKVercionCode()
                                                    , aui.getLatestClientAPKFileSize()
                                                    , aui.getLatestClientAPKURL());
                                            try {
                                                up.doUpdate();
                                            } catch (Exception e) {
                                                WidgetUtils.showToast(context, $$(R.string.login_form_version_error), WidgetUtils.ToastType.ERROR);
                                                Log.d(TAG, "新版下载时遇到错误，" + e.getMessage(), e);
                                            }
                                        }
                                    }
                                })
                                .setNegativeButton($$(R.string.login_form_have_latest_version_ignore),  new DialogInterface.OnClickListener(){
                                    @Override
                                    public void onClick(DialogInterface dialog, int which)
                                    {
                                        // 检查看看是否有强制更新限制
                                        // ################# 关于现在的更新机制说明：
                                        // ## 为何要用如此曲折的方式实现？不直接在AotuUpdate里实现？因为未来要对自动更新
                                        // ## 添加信息的话，重新编译AutoUpdateInfoFromServer对象必然导致客户端的序列化对象不
                                        // ## 一致错误，从而没法继续再用客户端，除非手动下载更新。最好的方法变是AutoUpdateInfoFromServer
                                        // ## 类从此不变化（从KLink2.0正式版发布后），以后万一哪 一天有新需求的话通过登陆认证对象里的
                                        // ## 保留字段来曲折实现即可，为何不在AutoUpdateInfoFromServer加保留字段呢？因为想加之前，测试版
                                        // ## 也已经不能重新改动AutoUpdateInfoFromServer，否则测试版就会更新不成功，所以干脆就这样了！！
                                        // 服务端有强制更新需求
                                        if(userAuthedInfo != null && userAuthedInfo.getMustVersion4A() != null)
                                        {
                                            // 强制更新至的版本
                                            int enforeUpdateToVersion = CommonUtils.getIntValue(userAuthedInfo.getMustVersion4A());//(Integer)userAuthedInfo.get_ex3();
                                            // 强制更新说明
                                            String enforeUpdateToVersionDesc = (String)userAuthedInfo.getMustVersionDesc4A();
                                            // 如果本地版本号小于服务端要求的版本号
                                            if(getAPKVersionCode() < enforeUpdateToVersion)
                                            {
                                                Activity parentActivity = getParentActivity();
                                                if(parentActivity != null) {
                                                    // 给一个提示框，且不让用户正常登陆聊天服务器
                                                    new AlertDialog.Builder(parentActivity)
                                                            .setTitle("您必须更新此版本")
                                                            .setMessage(enforeUpdateToVersionDesc)
                                                            .setPositiveButton("确认", null)
                                                            .show();
                                                }
                                                return;
                                            }
                                        }

                                        // 没有强制更新需求，无条件连接IM服务器
                                        //---------------------------------------------------------------------------- [2]处代码与[1]是一样的 S
                                        // 把本地用户信息保存起来备用哦
                                        MyApplication.getInstance(context).getIMClientManager().setLocalUserInfo(userAuthedInfo);

                                        // 更新检查（处理）完成后开始进入连接聊天服务器的过程
                                        doLoginIMServer(getParentActivity(), userAuthedInfo.getUser_uid(), userAuthedInfo.getToken(), afterLoginIMServerSucessObs);
                                        //---------------------------------------------------------------------------- [2]处代码与[1]是一样的 E
                                    }
                                })
                                .show();
                    }
                    else
                    {
                        // 没有强制更新需求，无条件连接IM服务器
                        //---------------------------------------------------------------------------- [1]处代码与[2]是一样的 S
                        // 把本地用户信息保存起来备用哦
                        MyApplication.getInstance(context).getIMClientManager().setLocalUserInfo(userAuthedInfo);

                        // 更新检查（处理）完成后开始进入连接聊天服务器的过程
                        doLoginIMServer(getParentActivity(), userAuthedInfo.getUser_uid(), userAuthedInfo.getToken(), afterLoginIMServerSucessObs);
                        //---------------------------------------------------------------------------- [1]处代码与[2]是一样的 E
                    }
                }
            }
            else
            {
                Log.e(TAG, "服务端返回了无效的登陆反馈信息，result="+ result);
            }
        }

        /**
         * 本方法将在异步任务执行失败后被调用（本方法默认什么也不做）。
         *
         * @param result
         * @since 6.0
         */
        @Override
        protected void onPostExecuteFailImpl(Object result)
        {
            if(this.afterLoginHttpServerFailObs != null)
                this.afterLoginHttpServerFailObs.update(null, null);
        }
    }

    /**
     * 本方法是本IM中唯一正确的连接到IM socket长连接服务器的途径。
     *
     * @param loginUserId 用于连接IM服务器时作为唯一用户id使用
     * @param loginToken 用于连接IM服务器时作为身份验证之用（此token通常由先前的
     * SSO单点登陆接口返回并定义接下来的验证策略）
     *
     * @see OnLoginIMServerDialogProgress
     */
    public static void doLoginIMServer(final Activity parentActivity
            , final String loginUserId, final String loginToken, final Observer afterLoginIMServerSucessObs)
    {
        // * 发送IM socket长连接请求
        new LocalDataSender.SendLoginDataAsync(new PLoginInfo(loginUserId, loginToken))
        {
            private String $$(int stringResId)
            {
                return parentActivity.getResources().getString(stringResId);
            }

            @Override
            protected void fireAfterSendLogin(int code)
            {
                // 表示指令成功发出
                if(code == 0)
                {
                    // socket长连接建立失败后的观察者（用于通知用户决定是否需要重试）
                    Observer retryObsrver = new Observer(){
                        @Override
                        public void update(Observable observable, Object data) {
                            // 本观察者中由用户选择是否重试长连接或者取消长连接重试
                            new AlertDialog.Builder(parentActivity)
                                    .setCancelable(false)
                                    .setTitle($$(R.string.login_form_connect_to_chatserver_timeout_title))
                                    .setMessage($$(R.string.login_form_connect_to_chatserver_timeout_content))
                                    // 确认按钮
                                    .setPositiveButton($$(R.string.login_form_connect_to_chatserver_timeout_retry), new DialogInterface.OnClickListener(){
                                        @Override
                                        public void onClick(DialogInterface dialog,int which) {
                                            // 重试时（再次尝试长连接建立哦）
                                            doLoginIMServer(parentActivity, loginUserId, loginToken, afterLoginIMServerSucessObs);
                                        }
                                    })
                                    // 取消按钮
                                    .setNegativeButton($$(R.string.general_cancel), new DialogInterface.OnClickListener(){
                                        @Override
                                        public void onClick(DialogInterface dialog,int which) {
                                            // * 取消显示IM长连接服务的连接进度条
                                            OnLoginIMServerDialogProgress.getInstance(parentActivity).showProgressDialogForPairing(false);
                                        }
                                    })
                                    .show();
                        }
                    };

                    // socket长连接建立成后的观察者（用于连接成功后要做的事）
                    Observer loginOkForLaunchObserver = new Observer(){
                        @Override
                        public void update(Observable observable, Object data)
                        {
                            // * 取消显示连接IM进度条
                            OnLoginIMServerDialogProgress.getInstance(parentActivity).showProgressDialogForPairing(false);

                            int code = (Integer)data;
                            // IM连接成功
                            if(code == 0)
                            {
                                if(afterLoginIMServerSucessObs != null)
                                    afterLoginIMServerSucessObs.update(null, null);
                            }
                            else
                            {
                                new AlertDialog.Builder(parentActivity)
                                        .setCancelable(false)
                                        .setTitle(R.string.general_error)
                                        .setMessage("Connected to chat server failure("+code+")")
                                        .setPositiveButton(R.string.general_ok, null).show();
                            }
                        }
                    };

                    // * 显示登陆连接IM长连接服务的进度条
                    OnLoginIMServerDialogProgress.getInstance(parentActivity).showProgressDialogForPairing(true);
                    // * 当登陆连接IM长连接服务时间超过超时时间后，就判定为本次登陆socket长连接服务超时，将通知本观察者
                    OnLoginIMServerDialogProgress.getInstance(parentActivity).setObsrver(retryObsrver);
                    // * 设置登陆连接IM socket长连接UI界面的观察者（当客户端收到服务端反馈过来的IM socket长连接成功消息时将通知本观察者）
                    MyApplication.getInstance(parentActivity).getIMClientManager().getBaseEventListener().setLoginOkForLaunchObserver(loginOkForLaunchObserver);

                    Log.d(TAG, "登陆IM长连接服务器的指令已成功发出！");
                }
                else
                {
                    WidgetUtils.showToast(parentActivity, "socket长连接失败，请检查您的网络！", WidgetUtils.ToastType.ERROR);
                    // * 出错时当然无条件取消显示连接IM的进度条
                    OnLoginIMServerDialogProgress.getInstance(parentActivity).showProgressDialogForPairing(false);
                }
            }
        }.execute();
    }

    /**
     * 用户成功进行http认证以及im长连接后将要调用的方法（本方法只是一个默认实现，如您的需求有变，可以在不用调用本方法）.
     */
    public static void afterLoginIMServerSucess(Activity parentActivity)
    {
        // 前往门户主页
        parentActivity.startActivity(IntentFactory.createPortalIntent(parentActivity));//IntentFactory.createChatIntent(LoginActivity.this));

        //############################################################################ 新音视频框架！ S
        try // TODO 此try catch要删除哦，目前是为了避免新音视频框架中的bug而临时为之（就是错误地转换ipv6地址的bug）
        {
            // 登陆音视频聊天服务器
            VVP2PProvider.getInstance(MyApplication.getInstance(parentActivity))
                    .login(MyApplication.getInstance(parentActivity).getIMClientManager().getLocalUserInfo().getUser_uid());
        }
        catch (Exception e)
        {
        }
        //############################################################################ 新音视频框架！ E

        //
        parentActivity.finish();
    }

    public static LoginInfo2 constructLoginInfo(Activity parentActivity, String loginName, String loginPsw)
    {
        BusinessIntelligence bi = new BusinessIntelligence(parentActivity);
        String biObj = JSON.toJSONString(bi, true);

        LoginInfo2 ai = new LoginInfo2();
        ai.setLoginName(loginName);
        ai.setLoginPsw(loginPsw);
        ai.setClientVersion(""+LoginHelper.getAPKVersionCode());//本字段时存放的就是客户端APK的真正版本码
        ai.setDeviceInfo(biObj);
        ai.setOsType("0");// 0：Android客户端，1：iOS客户端
        ai.setAppKey(Const.APPKey);

        return ai;
    }
}